package org.threads.connectionpool;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.threads.connectionpool.myconnection.ConnectionTracking;
import org.threads.connectionpool.myconnection.CreatingPoolException;
import org.threads.connectionpool.myconnection.MyConnection;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class ConnectionPool {

    private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionPool.class);

    private static final int WAITING_TIMEOUT = 100;
    private static final int IDLE_TIMEOUT = 1000;
    private static final int MAX_CONNECTIONS = 10;

    private final Semaphore sem = new Semaphore(MAX_CONNECTIONS, true);
    private final Queue<MyConnection> connections = new ConcurrentLinkedQueue<>();
    private final Queue<MyConnection> connectionsInUse = new ConcurrentLinkedQueue<>();

    private Thread timeoutTrackerThread;

    private Object lock = new Object();

    private class TimeoutTrackingThread extends Thread {

        @Override
        public void run() {
            while (true) {
                while (connectionsInUse.isEmpty()) {
                    try {
                        Thread.sleep(IDLE_TIMEOUT);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                for (MyConnection connectionTracking : connectionsInUse) {
                    if (System.currentTimeMillis() - connectionTracking.getLastTimeUsed() > IDLE_TIMEOUT) {
                        LOGGER.info("MyConnection [ ConnId = " + connectionTracking.getConnId()
                                + " ] was released on IDLE_TIMEOUT!");
                        checkin(connectionTracking);
                    }
                }
            }
        }
    }

    public ConnectionPool(String driverName, String URL, String user, String password) throws CreatingPoolException {

        try {

            Class.forName(driverName);
            for (int i = 0; i < MAX_CONNECTIONS; i++) {
                connections.add(new MyConnection(
                        DriverManager.getConnection(URL, user, password), i));
            }
        } catch (ClassNotFoundException e) {
            LOGGER.error("", e);
        } catch (SQLException e) {
            LOGGER.error("", e);
        } catch (Throwable e) {
            throw new CreatingPoolException(e.getMessage(), e);
        }

        timeoutTrackerThread = new TimeoutTrackingThread();
        timeoutTrackerThread.setDaemon(true);
    }

    public Connection checkout() {

        if (!timeoutTrackerThread.isAlive()) {
            timeoutTrackerThread.start();
        }

        try {
            sem.tryAcquire(WAITING_TIMEOUT, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            LOGGER.error(e.toString());
        }

        MyConnection res = connections.poll();
        if (res != null) {

            LOGGER.info(Thread.currentThread().getName() + ":  "
                    + "ConnectionTracking  [ ConnId = " + res.getConnId() + " ] has been obtained");

            connectionsInUse.add(res);
            return res;
        }

        return null;
    }

    public void checkin(Connection res) {

        if (res instanceof MyConnection) {

            if (connectionsInUse.contains(res)) {
                synchronized (lock) {
                    if (connectionsInUse.contains(res)) {

                        connections.add((MyConnection) res);
                        connectionsInUse.remove(res);
                        sem.release();

                        LOGGER.info(Thread.currentThread().getName() + ":  "
                                + "ConnectionTracking  [ ConnId = " + ((ConnectionTracking) res).getConnId() + " ] has been released");
                    }
                }
            }
        }
    }
}